Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic trait to combine UnixListener and TcpListener #4385

Merged
merged 17 commits into from
Jan 27, 2022

Conversation

cecton
Copy link
Contributor

@cecton cecton commented Jan 7, 2022

Motivation

I would like to have a generic type for creating a socket from TCP or from a Unix socket so I don't need to duplicate code.

Solution

  1. Added a new type that combines the SocketAddr of TcpListener and the one of UnixListener. I called it ListenAddr. [edit: opted for associated type]
  2. Added a new trait Listener with an accept() method and a local_addr() method to reflect the methods from TcpListener and UnixListener. [edit: also accept_poll()]
  3. Added a new trait AsyncReadWrite which combines AsyncRead and AsyncWrite. This is because I will need to box the stream. [edit: opted for associated type]
  4. Added a method set_nodelay() to reflect the method set_nodelay() of TcpStream which I needed. [edit: removed because it can be handled by the user easily]

Problem encountered

There is a module "udp" at root of tokio-util. It should probably moved to the new module "net" I created. One alternative would be to rename the "unix" module I created to something else.

Follow up of #4244

@Darksonn Darksonn added the A-tokio-util Area: The tokio-util crate label Jan 7, 2022
Comment on lines 8 to 14
#[derive(Debug)]
pub enum ListenAddr {
/// Socket address.
SocketAddr(std::net::SocketAddr),
/// Unix socket.
UnixSocket(tokio::net::unix::SocketAddr),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be an associated type of the Listener trait like how Io is set up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9bda024

Comment on lines 28 to 40
/// A trait for a listener: `TcpListener` and `UnixListener`.
pub trait Listener: Send + Unpin {
/// The stream's type of this listener.
type Io: tokio::io::AsyncRead + tokio::io::AsyncWrite;

/// Accepts a new incoming connection from this listener.
fn accept<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<(Self::Io, ListenAddr)>> + Send + 'a>>;

/// Returns the local address that this listener is bound to.
fn local_addr(&self) -> Result<ListenAddr>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the trait should look more like this:

Suggested change
/// A trait for a listener: `TcpListener` and `UnixListener`.
pub trait Listener: Send + Unpin {
/// The stream's type of this listener.
type Io: tokio::io::AsyncRead + tokio::io::AsyncWrite;
/// Accepts a new incoming connection from this listener.
fn accept<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<(Self::Io, ListenAddr)>> + Send + 'a>>;
/// Returns the local address that this listener is bound to.
fn local_addr(&self) -> Result<ListenAddr>;
}
/// A trait for a listener: `TcpListener` and `UnixListener`.
pub trait Listener {
/// The stream's type of this listener.
type Io: tokio::io::AsyncRead + tokio::io::AsyncWrite;
/// The address of the new connection.
type Addr;
fn poll_accept(self: Pin<&mut self>) -> Poll<(Self::Io, Self::ListenAddr)>;
/// Returns the local address that this listener is bound to.
fn local_addr(&self) -> Result<ListenAddr>;
/// Accepts a new incoming connection from this listener.
fn accept<'a>(&'a mut self) -> ListenerAcceptFut<'a, Self>
where
Self: Sized + Unpin,
{
ListenerAcceptFut::new(self)
}
}

Then, the crate would define a ListenAcceptFut type that implements Future with a manual impl similar to the return types in the Ext traits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c2ec467

@cecton
Copy link
Contributor Author

cecton commented Jan 12, 2022

I'm trying out with the current API using type alias but it seems it doesn't work with my use case because the TCP address/Unix socket path is provided at runtime so I would need to dynamic dispatch somewhere. Box<dyn Listener> doesn't work because the type aliases are required.

@Darksonn
Copy link
Contributor

You can implement the trait for the Either type also in tokio-util to get what you want.

@cecton
Copy link
Contributor Author

cecton commented Jan 12, 2022

Oh I think I see! Thanks!

@cecton cecton marked this pull request as ready for review January 13, 2022 11:47
@cecton
Copy link
Contributor Author

cecton commented Jan 13, 2022

@Darksonn ok I think I'm ready for review

@cecton
Copy link
Contributor Author

cecton commented Jan 13, 2022

(Fixed clippy stuff)

tokio-util/src/net/mod.rs Outdated Show resolved Hide resolved
Comment on lines 19 to 23
/// Polls to accept a new incoming connection to this listener.
fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>>;

/// Accepts a new incoming connection from this listener.
fn accept(&self) -> ListenerAcceptFut<'_, Self>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best to accept a &mut self here. Calling accept twice in parallel will result in one of them breaking due to lost wakeups.

Suggested change
/// Polls to accept a new incoming connection to this listener.
fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>>;
/// Accepts a new incoming connection from this listener.
fn accept(&self) -> ListenerAcceptFut<'_, Self>
/// Polls to accept a new incoming connection to this listener.
fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>>;
/// Accepts a new incoming connection from this listener.
fn accept(&mut self) -> ListenerAcceptFut<'_, Self>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would make the API different from tokio's TCP listener and Unix listener. If they don't use &mut self there, why would I need it here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accept fn in Tokio will not have the same lost wakeup issue as this one does because it isn't implemented by calling poll_accept on itself. However, we cant really replicate what Tokio does with a trait. The &mut self avoids the footgun.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I see!! Thanks for the explanation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fe231ec

Comment on lines 19 to 20
/// Polls to accept a new incoming connection to this listener.
fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take a pinned reference to self.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replicated the signature of the function from tokio's TCP listener. Are you sure you want to make it different?

Copy link
Contributor

@Darksonn Darksonn Jan 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, making this take &self will make it difficult to implement this trait for anything else than the two listener types in Tokio itself. This is why I suggested changing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I see

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fe231ec

Comment on lines 3 to 18
use super::Listener;
use std::io::Result;
use std::task::{Context, Poll};

impl Listener for tokio::net::UnixListener {
type Io = tokio::net::UnixStream;
type Addr = tokio::net::unix::SocketAddr;

fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>> {
Self::poll_accept(self, cx)
}

fn local_addr(&self) -> Result<Self::Addr> {
self.local_addr().map(Into::into)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just put this in the other folder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will force me to add #[cfg(unix)].

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't mind whether it is separate or together then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'll keep it then but I don't mind either way either

@cecton
Copy link
Contributor Author

cecton commented Jan 14, 2022

I think this is ready for review again.

@Darksonn
Copy link
Contributor

Your branch is 59 commits behind tokio-rs:master and is therefore missing a fix for the CI failure in the two FreeBSD CI runs. Please merge master into your branch.

Comment on lines 19 to 26
/// Polls to accept a new incoming connection to this listener.
fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>>;

/// Accepts a new incoming connection from this listener.
fn accept(&mut self) -> ListenerAcceptFut<'_, Self>
where
Self: Sized + Unpin,
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either poll_accept should take self: Pin<&mut Self>, or the Unpin bound should be removed from accept.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a538f0b

@cecton
Copy link
Contributor Author

cecton commented Jan 14, 2022

Your branch is 59 commits behind tokio-rs:master and is therefore missing a fix for the CI failure in the two FreeBSD CI runs. Please merge master into your branch.

oops 😅 I didn't notice somehow, sorry! Fixed in 812dfa5

And ready for review.

tokio-util/src/net/mod.rs Outdated Show resolved Hide resolved
Comment on lines 81 to 90
fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll<Result<(Self::Io, Self::Addr)>> {
match self {
Either::Left(listener) => listener
.poll_accept(cx)
.map(|res| res.map(|(stream, addr)| (Either::Left(stream), Either::Left(addr)))),
Either::Right(listener) => listener
.poll_accept(cx)
.map(|res| res.map(|(stream, addr)| (Either::Right(stream), Either::Right(addr)))),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. It's unfortunate that the types here allow it return stuff like (Left(io), Right(addr)) even though it wont in practice. Ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow I'm so blind I didn't even notice.

Yes you're right in practice this will never happen. If someones use match on the Result, they will need to make unreachable branches just for that. It's stupid.

.... 🤔 I will think about it... but I don't see a solution right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably making yet another Either specialized for Listener could solve the issue.

I noticed the Either type is something not universal anyway: tokio-util has one but tower also has its own: https://docs.rs/tower/latest/tower/util/enum.Either.html

I assume we could have a "EitherListener" with a poll_accept method that would return Result<Either<(IoLeft, AddrLeft), (IoRight, AddrRight)>>. The enum itself would look like this I guess:

pub enum EitherListener<L: Listener, R: Listener> {
    Left(L),
    Right(R),
}

I don't think the user absolutely "needs" to use tokio-util's Either, it could definitely be something more specialized. wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that adding an EitherListener enum changes anything here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EitherListener would not implement Listener, it would have its own method with their own signatures

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to change the trait so that Addr and Io are merged into a single type which each listener will define as a pair.

That would work too but not super elegant I guess...

You could just put the method on the Either we already have.

Good point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I replaced the Listener impl on Either by custom methods here: b55b62c3

Unfortunately the accept Future struct is slightly different so I had to duplicate some code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just make it an async fn and drop the poll function? We don't need these tricks when it's not a trait method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 316f015

tokio-util/src/net/mod.rs Outdated Show resolved Hide resolved
@Darksonn Darksonn merged commit 8f77ee8 into tokio-rs:master Jan 27, 2022
@cecton cecton deleted the cecton-unix-tcp-listener branch January 27, 2022 16:48
@cecton
Copy link
Contributor Author

cecton commented Jan 27, 2022

Thank you for your time

@Darksonn
Copy link
Contributor

Thank you for the PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio-util Area: The tokio-util crate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants